Skip to content

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

Release Notes

  • New Features

    • Fine-grained authorization controls for client management, enabling view, manage, configure, and role-mapping permissions at the client level
    • Support for isolated permission scopes across individual clients and client scopes
    • Enhanced permission model for granular admin access control over client operations
  • Tests

    • Added comprehensive test coverage for client permission management scenarios

…valuation implementation for ClientsPermissionsV2

Closes #35564

Signed-off-by: Martin Kanis <mkanis@redhat.com>
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This pull request introduces fine-grained authorization for Clients in Keycloak's admin permissions system. It adds a new CLIENTS resource type with scopes for configure, manage, view, and role-mapping operations, along with concrete permission evaluation logic and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Authorization Schema & Admin Permissions
server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java, services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java
Introduces CLIENTS resource type with configure, manage, map-roles, and view scopes. Adds feature-flagged event listener logic for ClientRemovedEvent, GroupRemovedEvent, and RoleRemovedEvent handling under ADMIN_FINE_GRAINED_AUTHZ gate.
Client Permission Evaluation
services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java, services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionsV2.java
Defines new ClientPermissionEvaluator interface with 20+ methods for granular client and client-scope permission checks. Implements ClientPermissionsV2 class providing concrete permission evaluation using authorization provider and resource/permission models.
Management Permissions Integration
services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissionsV2.java
Adds lazy-initialized clients() accessor method following existing users() pattern for unified permission access.
Test Infrastructure Refactoring
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/AbstractPermissionTest.java, tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionRESTTest.java, tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/UserResourceTypeEvaluationTest.java, tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/UserResourceTypePermissionTest.java
Refactors test helper methods from instance-based to static with explicit ManagedClient/Realm parameters. Adds policy/permission creation utilities (createUserPolicy, createClientPolicy, createAllPermission). Updates all test invocations to pass client context.
Client Permission Tests
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java
Adds comprehensive test coverage with 5 new test methods validating manage, configure, and view permissions for single and multiple clients, including role-mapping and client-scope operations.

Sequence Diagram(s)

sequenceDiagram
    participant TestCode as Test Code
    participant PermEval as ClientPermissionEvaluator
    participant PermV2 as ClientPermissionsV2
    participant AuthzProvider as AuthorizationProvider
    participant ResourceServer as ResourceServer

    TestCode->>PermV2: canManage(ClientModel client)
    PermV2->>PermV2: hasPermission(MANAGE, client)
    PermV2->>AuthzProvider: evaluate(evaluationContext)
    AuthzProvider->>ResourceServer: check permission against resource
    ResourceServer-->>AuthzProvider: permission result
    AuthzProvider-->>PermV2: evaluation result
    PermV2-->>TestCode: boolean result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A new CLIENTS realm unfolds today,
With CONFIGURE and MANAGE at play,
Fine-grained scopes dance and align,
Tests refactored in patterns divine,
Authorization hops bright on its way! 🌟

🚥 Pre-merge checks | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is minimal and does not follow the template structure. It only states this is an automatically created batch PR without explaining the actual changes, objectives, or implementation details. Provide a comprehensive description that explains what feature/functionality is being added, why it's needed, and how the changes relate to the feature-clients-authz-implementation.
Title check ❓ Inconclusive The title 'Automated Test: feature-clients-authz-implementation' is vague and generic. It doesn't clearly describe the primary change or new functionality being added to the codebase. Provide a more descriptive title that highlights the main feature being implemented, such as 'Add ClientPermissionsV2 and fine-grained authorization for clients' or similar.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature-clients-authz-implementation

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In
`@services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java`:
- Around line 95-98: Update the JavaDoc for the interface method
ClientPermissionEvaluator.requireView() to stop saying "Returns {`@code` true}"
and instead document that the method throws a ForbiddenException if {`@link`
`#canView`()} returns {`@code` false}; mirror the wording used by other requireXxx
methods (e.g., requireManage(), requireConfigure()) so the comment correctly
reflects the void signature and the thrown exception behavior.
- Around line 100-103: The JavaDoc for requireViewClientScopes() erroneously
says "Returns {`@code` true}" though the method is void; update its Javadoc to
match the semantics used in requireView() (e.g., state that the method enforces
the permission check and will throw an exception if the caller lacks permission
or simply requires view permissions) and remove any "Returns" phrasing so the
comment accurately describes the behavior of requireViewClientScopes().

In
`@tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java`:
- Around line 173-178: The test uses a hardcoded index get(1) on
defaultClientScopes which risks IndexOutOfBounds; change the access to a safe
index or guard it: verify defaultClientScopes is non-empty and select an element
(e.g., defaultClientScopes.get(0)) or add an explicit bounds check before
calling clientResource.removeDefaultClientScope(clientScopeRep.getId()) and
clientResource.addOptionalClientScope(clientScopeRep.getId()) in
PermissionClientTest (referencing defaultClientScopes,
clientResource.removeDefaultClientScope, and
clientResource.addOptionalClientScope).
- Around line 319-322: The test currently calls
realmAdminClient.realm(realm.getName()).clients().create(null) which can trigger
server-side NPE/400; replace the null with a minimal valid ClientRepresentation
(populate required fields like clientId and any mandatory flags) and use that
when calling ClientsResource.create(...) in PermissionClientTest to ensure the
request reaches authorization logic and still returns Status.FORBIDDEN; update
the assertion to keep expecting Status.FORBIDDEN.getStatusCode().
- Around line 147-153: The test assumes there are at least two default client
scopes by calling clientResource.getDefaultClientScopes().get(1), which can
throw IndexOutOfBoundsException; update PermissionClientTest to first check the
size of clientResource.getDefaultClientScopes() (or pick a safe element such as
index 0 or the first element) before calling get(...), and only call
clientResource.removeDefaultClientScope(clientScopeRep.getId()) when a valid
ClientScopeRepresentation is found (or assert/precondition the list size),
keeping the existing exception assertion around removeDefaultClientScope to
verify ForbiddenException.
🧹 Nitpick comments (4)
server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java (1)

304-326: Missing CLIENTS handling in getResourceName method.

The getResourceName method handles the USERS resource type to return human-readable names (e.g., "All users" or the username), but there's no equivalent handling for the new CLIENTS resource type. When displaying resource names for client permissions, this will fall through to returning resource.getDisplayName(), which may not be as user-friendly.

Consider adding similar handling for CLIENTS to return the client ID or "All clients" for consistency.

♻️ Proposed enhancement
                 return user.getUsername();
             }
+        } else if (CLIENTS.getType().equals(resourceType)) {
+            if (resource.getName().equals(CLIENTS_RESOURCE_TYPE)) {
+                return "All clients";
+            }
+
+            ClientModel client = session.clients().getClientById(session.getContext().getRealm(), resource.getName());
+
+            if (client == null) {
+                throw new ModelIllegalStateException("Client not found for resource [" + resource.getId() + "]");
+            }
+
+            return client.getClientId();
         }
     }
services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionsV2.java (2)

224-235: Minor: Consider using List.of() instead of Arrays.asList() for single-element lists.

The code creates a list with a single element using Arrays.asList(scope). Using List.of(scope) is more concise and returns an immutable list, which is appropriate here since the list isn't modified.

♻️ Proposed improvement
-        List<String> expectedScopes = Arrays.asList(scope);
+        List<String> expectedScopes = List.of(scope);

Also applies to: 249-260


262-272: Remove unused method getEvaluationContext.

The getEvaluationContext method (lines 262-272) is never called within this class or elsewhere in the codebase. Remove it to eliminate dead code.

tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java (1)

22-30: Mixing JUnit 4 and JUnit 5 imports.

Line 25 imports org.junit.Assert.fail (JUnit 4), but other assertions use JUnit 5 (org.junit.jupiter.api.Assertions). This inconsistency can cause confusion and potential issues if the test runner changes.

Proposed fix
-import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.fail;

Comment on lines +95 to 98
/**
* Returns {@code true} if {@link #canView()} returns {@code true}.
*/
void requireView();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

JavaDoc incorrectly states "Returns true" for a void method.

The JavaDoc for requireView() says "Returns {@code true} if..." but this method has a void return type. Based on the pattern of other requireXxx methods in this interface (e.g., lines 47-50, 59-62), this should say "Throws ForbiddenException if {@link #canView()} returns {@code false}."

📝 Proposed fix
-    /**
-     * Returns {`@code` true} if {`@link` `#canView`()} returns {`@code` true}.
-     */
+    /**
+     * Throws ForbiddenException if {`@link` `#canView`()} returns {`@code` false}.
+     */
     void requireView();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Returns {@code true} if {@link #canView()} returns {@code true}.
*/
void requireView();
/**
* Throws ForbiddenException if {`@link` `#canView`()} returns {`@code` false}.
*/
void requireView();
🤖 Prompt for AI Agents
In
`@services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java`
around lines 95 - 98, Update the JavaDoc for the interface method
ClientPermissionEvaluator.requireView() to stop saying "Returns {`@code` true}"
and instead document that the method throws a ForbiddenException if {`@link`
`#canView`()} returns {`@code` false}; mirror the wording used by other requireXxx
methods (e.g., requireManage(), requireConfigure()) so the comment correctly
reflects the void signature and the thrown exception behavior.

Comment on lines +100 to 103
/**
* Returns {@code true} if {@link #canViewClientScopes()} returns {@code true}.
*/
void requireViewClientScopes();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

JavaDoc incorrectly states "Returns true" for a void method.

Similar to requireView(), the JavaDoc for requireViewClientScopes() incorrectly describes the return behavior for a void method.

📝 Proposed fix
-    /**
-     * Returns {`@code` true} if {`@link` `#canViewClientScopes`()} returns {`@code` true}.
-     */
+    /**
+     * Throws ForbiddenException if {`@link` `#canViewClientScopes`()} returns {`@code` false}.
+     */
     void requireViewClientScopes();
🤖 Prompt for AI Agents
In
`@services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java`
around lines 100 - 103, The JavaDoc for requireViewClientScopes() erroneously
says "Returns {`@code` true}" though the method is void; update its Javadoc to
match the semantics used in requireView() (e.g., state that the method enforces
the permission check and will throw an exception if the caller lacks permission
or simply requires view permissions) and remove any "Returns" phrasing so the
comment accurately describes the behavior of requireViewClientScopes().

Comment on lines +147 to +153
try {
ClientScopeRepresentation clientScopeRep = clientResource.getDefaultClientScopes().get(1);
clientResource.removeDefaultClientScope(clientScopeRep.getId());
fail("Expected exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential IndexOutOfBoundsException when accessing index 1.

getDefaultClientScopes().get(1) assumes at least 2 default client scopes exist. If the test environment has fewer scopes, this will throw an IndexOutOfBoundsException.

Proposed fix
         try {
-            ClientScopeRepresentation clientScopeRep = clientResource.getDefaultClientScopes().get(1);
+            List<ClientScopeRepresentation> scopes = clientResource.getDefaultClientScopes();
+            if (scopes.isEmpty()) {
+                fail("Expected at least one default client scope");
+            }
+            ClientScopeRepresentation clientScopeRep = scopes.get(0);
             clientResource.removeDefaultClientScope(clientScopeRep.getId());
             fail("Expected exception wasn't thrown.");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
ClientScopeRepresentation clientScopeRep = clientResource.getDefaultClientScopes().get(1);
clientResource.removeDefaultClientScope(clientScopeRep.getId());
fail("Expected exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
try {
List<ClientScopeRepresentation> scopes = clientResource.getDefaultClientScopes();
if (scopes.isEmpty()) {
fail("Expected at least one default client scope");
}
ClientScopeRepresentation clientScopeRep = scopes.get(0);
clientResource.removeDefaultClientScope(clientScopeRep.getId());
fail("Expected exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
🤖 Prompt for AI Agents
In
`@tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java`
around lines 147 - 153, The test assumes there are at least two default client
scopes by calling clientResource.getDefaultClientScopes().get(1), which can
throw IndexOutOfBoundsException; update PermissionClientTest to first check the
size of clientResource.getDefaultClientScopes() (or pick a safe element such as
index 0 or the first element) before calling get(...), and only call
clientResource.removeDefaultClientScope(clientScopeRep.getId()) when a valid
ClientScopeRepresentation is found (or assert/precondition the list size),
keeping the existing exception assertion around removeDefaultClientScope to
verify ForbiddenException.

Comment on lines +173 to +178
// can remove a default client scope
ClientScopeRepresentation clientScopeRep = defaultClientScopes.get(1);
clientResource.removeDefaultClientScope(clientScopeRep.getId());

// can add an optional client scope
clientResource.addOptionalClientScope(clientScopeRep.getId());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same hardcoded index issue as above.

Line 174 also uses get(1) which has the same risk. Consider using get(0) or adding a bounds check.

🤖 Prompt for AI Agents
In
`@tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java`
around lines 173 - 178, The test uses a hardcoded index get(1) on
defaultClientScopes which risks IndexOutOfBounds; change the access to a safe
index or guard it: verify defaultClientScopes is non-empty and select an element
(e.g., defaultClientScopes.get(0)) or add an explicit bounds check before
calling clientResource.removeDefaultClientScope(clientScopeRep.getId()) and
clientResource.addOptionalClientScope(clientScopeRep.getId()) in
PermissionClientTest (referencing defaultClientScopes,
clientResource.removeDefaultClientScope, and
clientResource.addOptionalClientScope).

Comment on lines +319 to +322
// can't create a new client
try (Response response = realmAdminClient.realm(realm.getName()).clients().create(null)) {
Assertions.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Passing null to create() may cause unintended server-side errors.

Line 320 passes null as the ClientRepresentation, which could cause a NullPointerException or a 400 Bad Request instead of the expected 403 Forbidden. Use a valid representation to correctly test the authorization check.

Proposed fix
         // can't create a new client
-        try (Response response = realmAdminClient.realm(realm.getName()).clients().create(null)) {
+        ClientRepresentation newClient = new ClientRepresentation();
+        newClient.setClientId("shouldNotBeCreated");
+        try (Response response = realmAdminClient.realm(realm.getName()).clients().create(newClient)) {
             Assertions.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// can't create a new client
try (Response response = realmAdminClient.realm(realm.getName()).clients().create(null)) {
Assertions.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
// can't create a new client
ClientRepresentation newClient = new ClientRepresentation();
newClient.setClientId("shouldNotBeCreated");
try (Response response = realmAdminClient.realm(realm.getName()).clients().create(newClient)) {
Assertions.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
🤖 Prompt for AI Agents
In
`@tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java`
around lines 319 - 322, The test currently calls
realmAdminClient.realm(realm.getName()).clients().create(null) which can trigger
server-side NPE/400; replace the null with a minimal valid ClientRepresentation
(populate required fields like clientId and any mandatory flags) and use that
when calling ClientsResource.create(...) in PermissionClientTest to ensure the
request reaches authorization logic and still returns Status.FORBIDDEN; update
the assertion to keep expecting Status.FORBIDDEN.getStatusCode().

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant